package com.alcatel_lucent.osp.service;

import com.alcatel_lucent.osp.model.Request;
import com.alcatel_lucent.osp.model.RequestStatus;
import com.alcatel_lucent.osp.model.User;
import org.apache.commons.collections.PredicateUtils;
import org.apache.commons.collections.TransformerUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang.time.DurationFormatUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.net.util.Base64;
import org.apache.velocity.app.VelocityEngine;
import org.intellij.lang.annotations.Language;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.util.DigestUtils;

import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.apache.commons.collections.CollectionUtils.collect;
import static org.apache.commons.collections.CollectionUtils.filter;

@EnableScheduling
@Service("notificationService")
public class NotificationServiceImpl extends BaseServiceImpl implements NotificationService {

    private static Logger log = LoggerFactory.getLogger(NotificationServiceImpl.class);
    private DateFormat dFmt = DateFormat.getDateTimeInstance();

    public static final int ONE_WEEK = 3600 * 24 * 7;

    @Value("${smtp.host}")
    private String smtpHost;

    @Value("${smtp.port}")
    private Integer smtpPort;

    @Value("${smtp.auth}")
    private String mailAuth;

    @Value("${server.url}")
    private String serverUrl;

    @Value("${rd360.mailTemplates.exception}")
    private String rd360MailTemplateException;

    @Value("${notification.email}")
    private Boolean notificationEnabled;

    @Autowired
    private VelocityEngine velocityEngine;

    @Autowired
    private JavaMailSender mailSender;

    private List<String> rd360MailTemplateOther = Arrays.asList("Bug report", "Improvement", "New User Story");

    private String webRoot = ".";

    public String getWebRoot() {
        return webRoot;
    }

    public void setWebRoot(String webRoot) {
        this.webRoot = webRoot;
    }

    private Collection<String> getUserEmailCollection(Collection<User> users) {
        Collection<String> emailList = collect(users, TransformerUtils.invokerTransformer("getEmail"));
        filter(emailList, PredicateUtils.notNullPredicate());
        return emailList;
    }

    public void notifyNewRequest(Request request) {
        Collection<User> owners = new ArrayList<User>(request.getCategory().getOwners());
        if (org.springframework.util.CollectionUtils.isEmpty(owners)) {
            log.info("Request[ID={}] has no owner, abort email notification.", request.getId());
            return;
        }

        String subject = "[Operation Service Portal] New request of " + request.getCategory().getName() + ": #" + request.getReqNo();

        Collection<String> toList = getUserEmailCollection(owners);
        Collection<User> copyListUsers = request.getCopyList();

        Collection<String> ccList = getUserEmailCollection(copyListUsers);
        String categoryMailingList = request.getCategory().getMailingList();
        if (StringUtils.isNotBlank(categoryMailingList)) {
            ccList.addAll(Arrays.asList(categoryMailingList.split("\\s*;\\s*")));
        }
        //add requester
        if (StringUtils.isNotBlank(request.getReportBy().getEmail())) {
            ccList.add(request.getReportBy().getEmail());
        }

        Map<String, Object> model = new HashMap<String, Object>();
        model.put("request", request);
        model.put("serverUrl", serverUrl);

        try {
            sendMail(request.getReportBy().getEmail(), toList, ccList, model, "newRequest.vm", subject);
        } catch (Exception e) {
            log.warn("Failed to send notification. " + e.getMessage());
            e.printStackTrace();
//			throw new SystemError("Failed to send notification, please contact system admin!", e);
        }

    }

    public void notifyUpdateRequest(Request request, String changes, String comments, User user) {
        Collection<String> toList;
        Collection<String> ccList = new ArrayList<String>();
        Collection<User> owners = new ArrayList<User>(request.getCategory().getOwners());
        if (request.getReportBy().getCil().equals(user.getCil())) { // update by reporter, send to all owners
            toList = getUserEmailCollection(owners);
            ccList.add(request.getReportBy().getEmail());
        } else if (request.isOwner(user)) {    // update by owner, send to reporter and cc all other owners
            toList = getUserEmailCollection(Arrays.asList(request.getReportBy()));
            ccList.addAll(getUserEmailCollection(owners));
        } else {  // update by other user, such as super user
            toList = getUserEmailCollection(owners);
            toList.add(request.getReportBy().getEmail());
            ccList.add(user.getEmail());
        }
        ccList.addAll(getUserEmailCollection(request.getCopyList()));
        String categoryMailingList = request.getCategory().getMailingList();
        if (StringUtils.isNotBlank(categoryMailingList)) {
            ccList.addAll(Arrays.asList(categoryMailingList.split("\\s*;\\s*")));
        }

        if (toList.isEmpty()) {
            log.warn("Request[ID={}] to list is empty, abort send email notification.", request.getId());
            return;
        }
        String subject = "[Operation Service Portal] Update request of " + request.getCategory().getName() + ": #" + request.getReqNo();
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("request", request);
        model.put("changes", changes);
        model.put("comments", comments);
        model.put("serverUrl", serverUrl);
        try {
            sendMail(user.getEmail(), toList, ccList, model, "updateRequest.vm", subject);
        } catch (Exception e) {
            log.warn("Failed to send notification. " + e.getMessage());
            e.printStackTrace();
//			throw new SystemError("Failed to send notification, please contact system admin!", e);
        }
    }

    public void notifyNewRd360Request(Request request) {

        Collection<User> owners = new ArrayList<User>(request.getCategory().getOwners());
        if (org.springframework.util.CollectionUtils.isEmpty(owners)) {
            log.info("Request[ID={}] has no owner, abort email notification.", request.getId());
            return;
        }

        Collection<String> toList = getUserEmailCollection(owners);
        Collection<User> copyListUsers = request.getCopyList();

        Collection<String> ccList = getUserEmailCollection(copyListUsers);
        String categoryMailingList = request.getCategory().getMailingList();
        if (StringUtils.isNotBlank(categoryMailingList)) {
            ccList.addAll(Arrays.asList(categoryMailingList.split("\\s*;\\s*")));
        }
        //add requester
        if (StringUtils.isNotBlank(request.getReportBy().getEmail())) {
            ccList.add(request.getReportBy().getEmail());
        }

        //special case for rd 360 request
        if (request.getType().getName().equals(rd360MailTemplateException)) {
            //move original to list to cc list
            ccList.addAll(toList);
            toList.clear();
            toList.add("supportEPM@bsf.alcatel.fr");
        }

        Map<String, Object> params = getRd360MailParameters(request);
        try {
            sendMail(request.getReportBy().getEmail(), toList, ccList, (Map<String, Object>) params.get("model"),
                    "rd360/" + params.get("template"), (String) params.get("subject"));
        } catch (Exception e) {
            log.warn("Failed to send notification. " + e.getMessage());
            e.printStackTrace();
//			throw new SystemError("Failed to send notification, please contact system admin!", e);
        }
    }

    private Map<String, Object> getRd360MailParameters(Request request) {
        Map<String, Object> params = new HashMap<String, Object>();


        String type = request.getType().getName();
        String templateFile = (rd360MailTemplateOther.contains(type) ? "Other" : type.replaceAll(" ", "_")) + ".vm";

        params.put("template", templateFile);

        String subject;
        Map<String, Object> model = new HashMap<String, Object>();

        String projectName = request.getAttributes().get("attr_projectName");
        if (Arrays.asList("Disable a PWS", "Change EPM-based PWS to local").contains(type)) {
            projectName = request.get("attr_extra_project_name");
        }
        String projectType = request.get("attr_projectType");
        String projectTypeExt = RequestServiceImpl.rd360ProgramTypeMapping.get(projectType);

        model.put("projectExt", projectTypeExt);
        model.put("projectName", projectName);
        model.put("request", request);

        if (type.equals("Disable a PWS")) {
            subject = String.format("Request for disabling %s PWS", projectName);
        } else if (type.equals("Change EPM-based PWS to local")) {
            subject = String.format("Request for changing %s to local PWS", projectName);
        } else if (type.equals("Create a Local PWS")) {
            subject = String.format("Creation request of local <<%s_%s>> PWS", projectName, projectTypeExt);
        } else if (type.equals("Create a PWS based on EPM")) {
            subject = String.format("EPM Creation request for %s_%s", projectName, projectTypeExt);
            model.put("projectTemplate", projectType.equals("Offer") ? "Offer&nbsp;&nbsp;" : "NEW_PCT_");
            model.put("hierarchy", projectType.equals("Offer") ? "Offer" : "PMO");
        } else {
            subject = "R&D 360 Enterprise Request: " + request.getDescription();
        }

        params.put("model", model);
        params.put("subject", subject);

        return params;
    }

    @Override
    public void notifyUpdateRd360Request(Request request, String changes, String comments, User user) {
        Collection<String> toList;
        Collection<String> ccList = new ArrayList<String>();
        Collection<User> owners = new ArrayList<User>(request.getCategory().getOwners());
        if (request.getReportBy().getCil().equals(user.getCil())) { // update by reporter, send to all owners
            toList = getUserEmailCollection(owners);
            ccList.add(request.getReportBy().getEmail());
        } else if (request.isOwner(user)) {    // update by owner, send to reporter and cc all other owners
            toList = getUserEmailCollection(Arrays.asList(request.getReportBy()));
            ccList.addAll(getUserEmailCollection(owners));
        } else {  // update by other user, such as super user
            toList = getUserEmailCollection(owners);
            toList.add(request.getReportBy().getEmail());
            ccList.add(user.getEmail());
        }
        ccList.addAll(getUserEmailCollection(request.getCopyList()));
        String categoryMailingList = request.getCategory().getMailingList();
        if (StringUtils.isNotBlank(categoryMailingList)) {
            ccList.addAll(Arrays.asList(categoryMailingList.split("\\s*;\\s*")));
        }

        //special case for rd 360 request
        if (request.getType().getName().equals(rd360MailTemplateException)) {
            //move original to list to cc list
            ccList.addAll(toList);
            toList.clear();
            toList.add("supportEPM@bsf.alcatel.fr");
        }

        if (toList.isEmpty()) {
            log.warn("Request[ID={}] to list is empty, abort send email notification.", request.getId());
            return;
        }
        // get rd 360 new template
        String templateDir = "rd360/";
        Map<String, Object> params = getRd360MailParameters(request);

        Map<String, Object> model = (Map<String, Object>) params.get("model");
        model.put("request", request);
        model.put("changes", changes);
        model.put("comments", comments);
        model.put("serverUrl", serverUrl);

//        for update template
        model.put("updateSign", true);

        StringBuilder mailContent = new StringBuilder(VelocityEngineUtils.mergeTemplateIntoString(velocityEngine,
                templateDir + params.get("template"), "UTF-8", model));

        try {
            sendMail(user.getEmail(), toList, ccList, mailContent.toString(), (String) params.get("subject"));
        } catch (Exception e) {
            log.warn("Failed to send notification. " + e.getMessage());
            e.printStackTrace();
//			throw new SystemError("Failed to send notification, please contact system admin!", e);
        }
    }

    /**
     * Send mail
     *
     * @param from        send from
     * @param model       mail template variable model
     * @param vmFile      mail template attachment
     * @param subject     mail subject
     * @param mailTo      mail to
     * @param mailCc      mail cc
     * @param attachments mail attachments
     */
    public void sendMail(final String from, final Collection<String> mailTo, final Collection<String> mailCc,
                         final Map<String, Object> model, final String vmFile, final String subject,
                         final File... attachments) {
        //filter the duplicated addresses in mailTo and mailCc
        String content = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, vmFile, "UTF-8", model);
        sendMail(from, mailTo, mailCc, content, subject, attachments);
    }

    /**
     * Create a message from an HTML string.
     * Automatically makes modifications for inline images and backgrounds
     * and creates a plain-text version by converting the HTML.
     * Overwrites any existing values in $this->Body and $this->AltBody
     *
     * @access public
     * @param text $message HTML message string
     * @return map store inline resources and replaced text.
     */
    private static Pattern IMAGE_PATTERN = Pattern.compile("(?i)(src|background)=[\"\'](.*?)[\"\']");
    private static Pattern DATA_URL_PATTERN = Pattern.compile("^data:(image[^;,]*)(;base64)?,");
    private static Pattern ABSOLUTE_URL_PATTERN = Pattern.compile("^[A-z]+://");

    private Map<String, ImmutablePair<String, Resource>> msgHtml(String text, String baseDir) {
        Matcher m = IMAGE_PATTERN.matcher(text);

        Map<String, ImmutablePair<String, Resource>> resources = new HashMap<String, ImmutablePair<String, Resource>>();

        StringBuffer newText = new StringBuffer();
        while (m.find()) {
            String imgIndex = m.group(1);
            String url = m.group(2);

            Matcher urlMatcher = DATA_URL_PATTERN.matcher(url);
            //RFC2392 S 2
            String cid = DigestUtils.md5DigestAsHex(url.getBytes());
            Resource resource = null;
            String contentType = null;
            if (urlMatcher.find()) {
                String data = url.substring(url.indexOf(",") + 1);
                contentType = urlMatcher.group(1);
                //has group 2 means has ;base64, data need to be decoded from base64 encoding
                byte[] binData = null;
                if (urlMatcher.groupCount() > 1) {
                    binData = Base64.decodeBase64(data);
                } else {
                    try {
                        binData = URLDecoder.decode(data, "iso8859-1").getBytes("iso8859-1");
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
                resource = new ByteArrayResource(binData);
            } else if (!ABSOLUTE_URL_PATTERN.matcher(url).find()) {
                resource = new FileSystemResource(new File(baseDir, url));
            }
            m.appendReplacement(newText, imgIndex + "=\"cid:" + cid + "\"");
            resources.put(cid, ImmutablePair.of(contentType, resource));
        }
        m.appendTail(newText);

        Map ret = new HashMap();
        ret.put("newText", newText.toString());
        ret.put("resources", resources);

        return ret;
    }

    /**
     * Send mail
     *
     * @param from        send from
     * @param content     mail content
     * @param subject     mail subject
     * @param mailTo      mail to
     * @param mailCc      mail cc
     * @param attachments mail attachments
     */
    private void sendMail(final String from, final Collection<String> mailTo, final Collection<String> mailCc,
                          final String content, final String subject,
                          final File... attachments) {
        //filter the duplicated addresses in mailTo and mailCc
        HashSet<String> originalMailTo = new HashSet<String>(mailTo);
        mailTo.clear();
        mailTo.addAll(originalMailTo);

        HashSet<String> originalMailCc = new HashSet<String>(mailCc);
        originalMailCc.removeAll(originalMailTo);
        mailCc.clear();
        mailCc.addAll(originalMailCc);

        MimeMessagePreparator preparator = new MimeMessagePreparator() {
            @Override
            public void prepare(MimeMessage mimeMessage) throws Exception {
                MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
                messageHelper.setTo(mailTo.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                messageHelper.setSubject(subject);
                messageHelper.setFrom(from);
                if (mailCc != null) {
                    messageHelper.setCc(mailCc.toArray(ArrayUtils.EMPTY_STRING_ARRAY));
                }
                // merge velocity text
                Map result = msgHtml(content, webRoot);
                Map<String, ImmutablePair<String, Resource>> resources = (Map<String, ImmutablePair<String, Resource>>) result.get("resources");
                messageHelper.setText((String) result.get("newText"), true);

                // add inline resources

                Set<Map.Entry<String, ImmutablePair<String, Resource>>> entries = resources.entrySet();
                for (Map.Entry<String, ImmutablePair<String, Resource>> entry : entries) {
                    String cid = entry.getKey();
                    String contentType = entry.getValue().getLeft();
                    Resource resource = entry.getValue().getRight();
                    if (null == contentType) {
                        contentType = messageHelper.getFileTypeMap().getContentType(resource.getFile());
                    }
                    messageHelper.addInline(cid, resource, contentType);
                }
                // add attachments
                for (File attachment : attachments) {
                    messageHelper.addAttachment(attachment.getName(), attachment);
                }
            }
        };
        JavaMailSenderImpl sender = (JavaMailSenderImpl) mailSender;
        if (sender.getJavaMailProperties().getProperty("mail.smtp.auth").equals("true")) {
            if (null == sender.getUsername()) sender.setUsername(from);
            if (null == sender.getPassword()) sender.setPassword(StringUtils.EMPTY);
        }

        // log
        String logInfo = StringUtils.join(Arrays.asList(
                "",
                StringUtils.center(String.format("Email notification is %sabled", notificationEnabled ? "en" : "dis"), 150, '='),
                StringUtils.join(Arrays.asList("Send mail FROM: {}", "TO: {}", "CC: {}", "SUBJECT: {}", "BODY:", "{}"), '\n'),
                StringUtils.repeat("=", 150)
        ), '\n');

        log.info(logInfo, Arrays.asList(from, StringUtils.join(mailTo, ";"), StringUtils.join(mailCc, ";"), subject, content).toArray());
        if (!notificationEnabled) return;
        mailSender.send(preparator);
    }


    /**
     * Find all the requests which status is new and their status not updated more than period seconds..
     *
     * @param period the second period
     */
    private Collection<Request> findUntreatedRequests(int period, int reminderPeriod) {
        Date dateBeforePeriodSeconds = DateUtils.addSeconds(new Date(), -period);
        Date dateBeforeReminderPeriodSeconds = DateUtils.addSeconds(new Date(), -reminderPeriod);

        @Language("HQL") String hql = "from Request where reportTime < :reportTime and status = :status and (remindDate is null or remindDate < :remindPeriod)";
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("reportTime", dateBeforePeriodSeconds);
        params.put("status", RequestStatus.NEW);
        params.put("remindPeriod", dateBeforeReminderPeriodSeconds);
        return dao.retrieve(hql, params, new String[]{"category"});
    }

    /**
     * Execute everyday midnight
     */
    @Scheduled(cron = "0 0 0 * * *")
    @Override
    public void notifyUntreatedRequests() {
        log.info("====== Check untreated request ======", dFmt.format(new Date()));
        Collection<Request> requests = findUntreatedRequests(ONE_WEEK, ONE_WEEK);
        if (requests.isEmpty()) {
            log.info("No untreated requests need to be notified.");
            return;
        }

        for (Request request : requests) {
            remindRequestOwner(request);
        }
    }

    private void remindRequestOwner(Request request) {
        Collection<User> owners = new ArrayList<User>(request.getCategory().getOwners());
        if (org.springframework.util.CollectionUtils.isEmpty(owners)) {
            log.info("Request[ID={}] has no owner, abort email notification.", request.getId());
            return;
        }

        Collection<String> toList = getUserEmailCollection(owners);
        Collection<User> copyListUsers = request.getCopyList();

        Collection<String> ccList = getUserEmailCollection(copyListUsers);
        String categoryMailingList = request.getCategory().getMailingList();
        if (StringUtils.isNotBlank(categoryMailingList)) {
            ccList.addAll(Arrays.asList(categoryMailingList.split("\\s*;\\s*")));
        }
        //add requester
        if (StringUtils.isNotBlank(request.getReportBy().getEmail())) {
            ccList.add(request.getReportBy().getEmail());
        }

        String subject = String.format("[Operation Service Portal] Reminder request of %s: #%s", request.getCategory().getName(), request.getReqNo());

        Map<String, Object> model = new HashMap<String, Object>();
        model.put("request", request);
        model.put("period", StringUtils.replaceOnce(
                DurationFormatUtils.formatDuration(System.currentTimeMillis() - request.getReportTime().getTime(), "d' days"),
                " 1 days", " 1 day"));
        model.put("serverUrl", serverUrl);

        try {
            sendMail(request.getReportBy().getEmail(), toList, ccList, model, "reminderRequest.vm", subject);
            request.setRemindDate(new Timestamp(System.currentTimeMillis()));
        } catch (Exception e) {
            log.warn("Failed to send notification. " + e.getMessage());
            e.printStackTrace();
        }
    }
}
